跳到主要内容

react router源码学习

概述

react router的原理总结来说其实非常简单,基于Context透传history,同时使用path-to-regexp做 URL 路径解析来匹配渲染对应组件,over!

基本结构

react-router本身支持webreact native两个版本,平时常用的react-router-dom就是web版本的,react-router-native则是react native版本的,则两个都依赖于核心库react-router

对于react-router源码部分,结构也是一目了然,几个核心 API 都是和文件名称相关联的,唯一不足的就是没有基于typescript实现。

image-20210721222524954

Context

基于createNameContext生成不同的context实例,这里displayName属性可以方便在 React DevTools 中调试,React DevTools 使用该字符串来确定 context要显示的内容。

const createNamedContext = name => {
const context = createContext();
context.displayName = name;

return context;
};

然后在Router组件中会根据创建的context对象姓曾基础的Provider,于是使用Router组件包裹的内部组件都可以通过Context获取value。这里的props会在react-router-dom中经过处理,通过第三方库提供的createHistory方法来塑造history对象,透传下去。

// Router
const RouterContext = /*#__PURE__*/ createNamedContext('Router');
const HistoryContext = /*#__PURE__*/ createNamedContext('Router-History');

<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext,
}}
>
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
/>
</RouterContext.Provider>;

// react-router-dom
import { Router } from 'react-router';
import { createBrowserHistory as createHistory } from 'history';

class BrowserRouter extends React.Component {
history = createHistory(this.props);

render() {
return <Router history={this.history} children={this.props.children} />;
}
}

Switch

Switch组件会包裹一系列Route组件,用于根据 URL 渲染匹配的组件。

<Switch>
<Route exact path="/">
<Home />
</Route>

<Route path="/users">
<Users />
</Route>
<Redirect from="/accounts" to="/users" />

<Route>
<NoMatch />
</Route>
</Switch>

Switch组件中,首先需要获取Router传递的Context属性值,然后通过path-to-regexp来解析在Route

中指定的path或在Redirect中指定的from属性,来和当前 URL 匹配。

class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, 'You should not use <Switch> outside a <Router>');

const location = this.props.location || context.location;

let element, match;

// 这里使用 forEach 方法而不是 React.Children.toArray().find(),是因为 toArray
// 会默认给子元素追加 key 或者给子元素的 key 追加前缀,这样会在使不同 URL 渲染指定的
// 相同组件时,导致组件重新渲染
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;

const path = child.props.path || child.props.from;

// 如果匹配,则 match 会获得一个对象
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});

return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}

Switch关键部分在于matchPath这个方法,其内部使用了path-to-regexp来解析指定的匹配规则,返回一个正则表达式,例如

const keys = [];
const regexp = pathToRegexp('/foo/:bar');

// 结果
regexp = /^\/foo(?:\/([^\/#\?]+?))[\/#\?]?$/i;
keys = [
{
name: 'bar',
prefix: '/',
suffix: '',
pattern: '[^\\/#\\?]+?',
modifier: '',
},
];

通过在Switch中获取Route指定的匹配规则来生成正则表达式,然后使用正则表达式匹配当前 URL 的pathname

function matchPath(pathname, options = {}) {
const { path, exact = false, strict = false, sensitive = false } = options;

const { regexp, keys } = pathToRegexp(path, [], {
end: exact,
strict,
sensitive,
});

// 正则匹配
const match = regexp.exec(pathname);

// 不匹配直接返回 null
if (!match) return null;

const [url, ...values] = match;
const isExact = pathname === url;

if (exact && !isExact) return null;

return {
path,
url: path === '/' && url === '' ? '/' : url,
isExact,
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {}),
};
}

Route

Route用来指定匹配规则path和对应渲染的组件,也是需要消费Router提供的Context属性,并且可以在指定的组件是render props形式时,将这些属性传递到组件内部的props

class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const location = this.props.location || context.location;
const match = this.props.computedMatch
? this.props.computedMatch
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;

const props = { ...context, location, match };

// 三种渲染形式
let { children, component, render } = this.props;

return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === 'function'
? children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === 'function'
? children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}